package com.microtf.inscription.framework.service;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.microtf.inscription.framework.exception.BusinessException;
import com.microtf.inscription.framework.pojo.wechat.MessageTransfer;
import com.microtf.inscription.framework.pojo.wechat.input.ImageMessage;
import com.microtf.inscription.framework.pojo.wechat.input.LinkMessage;
import com.microtf.inscription.framework.pojo.wechat.input.LocationMessage;
import com.microtf.inscription.framework.pojo.wechat.input.Message;
import com.microtf.inscription.framework.pojo.wechat.input.ShortVideoMessage;
import com.microtf.inscription.framework.pojo.wechat.input.TextMessage;
import com.microtf.inscription.framework.pojo.wechat.input.VideoMessage;
import com.microtf.inscription.framework.pojo.wechat.input.VoiceMessage;
import com.microtf.inscription.framework.pojo.wechat.input.event.ClickEvent;
import com.microtf.inscription.framework.pojo.wechat.input.event.Event;
import com.microtf.inscription.framework.pojo.wechat.input.event.LocationEvent;
import com.microtf.inscription.framework.pojo.wechat.input.event.ScanEvent;
import com.microtf.inscription.framework.pojo.wechat.input.event.SubscribeEvent;
import com.microtf.inscription.framework.pojo.wechat.input.event.UnsubscribeEvent;
import com.microtf.inscription.framework.pojo.wechat.input.event.ViewEvent;
import com.microtf.inscription.framework.pojo.wechat.listener.EventListener;
import com.microtf.inscription.jpa.entity.MiniApp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import java.util.Random;

/**
 * 微信消息交互服务
 * 类主要包含用户关注，取消关注，扫码
 * 和发送消息接口接口。
 * 用户只需要实现消息接口就能实现交互
 *
 * @author guliuzhong
 * @see com.microtf.inscription.framework.pojo.wechat.input.event.Event
 */
@Service
@Slf4j
public class WeChatService extends WeChatServiceBase {
    /**
     * 微信公众号事件
     */
    ThreadLocal<EventListener> eventListenerThreadLocal = new ThreadLocal<>();
    /**
     * 是否使用加密协议
     */
    ThreadLocal<Boolean> bCryptThreadLocal = new ThreadLocal<>();
    XmlMapper xmlMapper = new XmlMapper();
    public void init(Integer appId, EventListener eventListener) {
        super.init(appId);
        if (eventListener != null) {
            eventListenerThreadLocal.set(eventListener);
        }
        bCryptThreadLocal.set(false);
    }
    public void release() {
        super.release();
        eventListenerThreadLocal.remove();
        bCryptThreadLocal.remove();
    }
    public String handleMessage(HttpServletRequest httpServletRequest) {
        MiniApp miniApp = miniAppThreadLocal.get();

        if (miniApp == null) {
            throw new BusinessException("应用App没有设置");
        }
        boolean checkSignature = checkSignature(httpServletRequest.getParameter("signature"), httpServletRequest.getParameter("timestamp"), httpServletRequest.getParameter("nonce"), "");
        if (!checkSignature) {
            return "";
        }
        if (httpServletRequest.getParameter("echostr") != null) {
            return httpServletRequest.getParameter("echostr");
        }
        try {
            String xmlMessage = unWarpCrypt(httpServletRequest);
            Optional<com.microtf.inscription.framework.pojo.wechat.output.Message> message = triggerEvent(xmlMessage);
            return warpCrypt(message);
        } catch (IOException e) {
            log.info("we chat IOException");
            e.printStackTrace();
        }
        log.info("we chat message");
        return "";
    }

    public String unWarpCrypt(HttpServletRequest httpServletRequest) throws IOException {
        String encryptType = httpServletRequest.getParameter("encrypt_type");
        ServletInputStream inputStream = httpServletRequest.getInputStream();
        if ("aes".equalsIgnoreCase(encryptType)) {
            bCryptThreadLocal.set(true);
            MessageTransfer messageTransfer = xmlMapper.readValue(inputStream, MessageTransfer.class);
            boolean checkSignature = checkSignature(httpServletRequest.getParameter("msg_signature"), httpServletRequest.getParameter("timestamp"), httpServletRequest.getParameter("nonce"), messageTransfer.getEncryptData());
            if (!checkSignature) {
                throw new BusinessException("Signature error");
            }
            WeChatBizCrypt wechatBizCrypt = new WeChatBizCrypt(miniAppThreadLocal.get());
            return wechatBizCrypt.decrypt(messageTransfer.getEncryptData());
        } else {
            bCryptThreadLocal.set(false);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

            byte[] readByte = new byte[1024];
            int len;
            while ((len = inputStream.read(readByte, 0, readByte.length)) > 0) {
                byteArrayOutputStream.write(readByte, 0, len);
            }
            return byteArrayOutputStream.toString();
        }
    }

    public String warpCrypt(Optional<com.microtf.inscription.framework.pojo.wechat.output.Message> message) throws IOException {
        if (!message.isPresent()) {
            log.info("没有消息返回到微信");
            return "";
        }
        if (bCryptThreadLocal.get()) {
            com.microtf.inscription.framework.pojo.wechat.output.Message message1 = message.get();
            WeChatBizCrypt wechatBizCrypt = new WeChatBizCrypt(miniAppThreadLocal.get());
            String s = xmlMapper.writeValueAsString(message.get());
            String encrypt = wechatBizCrypt.encrypt(wechatBizCrypt.getRandomStr(), s);
            MessageTransfer messageTransfer = new MessageTransfer();
            messageTransfer.setEncryptData(encrypt);
            messageTransfer.setToUserName(message1.getToUserName());
            String timeStamp = Long.toString(System.currentTimeMillis());
            messageTransfer.setNonce(wechatBizCrypt.getRandomStr());
            Optional<String> signature = getSignature(timeStamp, messageTransfer.getNonce(), encrypt);
            signature.ifPresent(messageTransfer::setMsgSignature);
            messageTransfer.setTimeStamp(timeStamp);
            return xmlMapper.writeValueAsString(messageTransfer);
        } else {
            return xmlMapper.writeValueAsString(message.get());
        }
    }

    /**
     * 事件分发
     *
     * @param xmlMsg 腾讯服务器发送的与用户交互的信息明文
     * @throws IOException 解析xml出错时报IOException错误
     */
    private <T extends com.microtf.inscription.framework.pojo.wechat.output.Message> Optional<T> triggerEvent(String xmlMsg) throws IOException {
        EventListener eventListener = eventListenerThreadLocal.get();
        if (eventListener == null) {
            return Optional.empty();
        }
        Message message = xmlMapper.readValue(xmlMsg, Message.class);
        switch (message.getMsgType()) {
            case "event":
                Event event = xmlMapper.readValue(xmlMsg, Event.class);
                switch (event.getEvent()) {
                    case "subscribe":
                        return eventListener.onEvent(xmlMapper.readValue(xmlMsg, SubscribeEvent.class));
                    case "unsubscribe":
                        return eventListener.onEvent(xmlMapper.readValue(xmlMsg, UnsubscribeEvent.class));
                    case "SCAN":
                        return eventListener.onEvent(xmlMapper.readValue(xmlMsg, ScanEvent.class));
                    case "LOCATION":
                        return eventListener.onEvent(xmlMapper.readValue(xmlMsg, LocationEvent.class));
                    case "CLICK":
                        return eventListener.onEvent(xmlMapper.readValue(xmlMsg, ClickEvent.class));
                    case "VIEW":
                        return eventListener.onEvent(xmlMapper.readValue(xmlMsg, ViewEvent.class));
                    default:
                        break;
                }
                break;
            case "text":
                return eventListener.onMessage(xmlMapper.readValue(xmlMsg, TextMessage.class));
            case "image":
                return eventListener.onMessage(xmlMapper.readValue(xmlMsg, ImageMessage.class));
            case "voice":
                return eventListener.onMessage(xmlMapper.readValue(xmlMsg, VoiceMessage.class));
            case "video":
                return eventListener.onMessage(xmlMapper.readValue(xmlMsg, VideoMessage.class));
            case "shortvideo":
                return eventListener.onMessage(xmlMapper.readValue(xmlMsg, ShortVideoMessage.class));
            case "location":
                return eventListener.onMessage(xmlMapper.readValue(xmlMsg, LocationMessage.class));
            case "link":
                return eventListener.onMessage(xmlMapper.readValue(xmlMsg, LinkMessage.class));
            default:
                break;
        }
        return Optional.empty();
    }

    private boolean checkSignature(String signature, String timestamp, String nonce, String encrypt) {
        Optional<String> signature1 = getSignature(timestamp, nonce, encrypt);
        return signature1.filter(signature::equalsIgnoreCase).isPresent();
    }

    private Optional<String> getSignature(String timestamp, String nonce, String encrypt) {
        MiniApp miniApp = miniAppThreadLocal.get();
        String[] paramArr = new String[]{miniApp.getToken(), timestamp, nonce, encrypt};
        Arrays.sort(paramArr);
        StringBuilder sb = new StringBuilder();
        for (String s : paramArr) {
            sb.append(s);
        }
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
            messageDigest.update(sb.toString().getBytes());
            byte[] digest = messageDigest.digest();
            StringBuilder hexStr = new StringBuilder();
            String shaHex;
            for (byte b : digest) {
                shaHex = Integer.toHexString(b & 0xFF);
                if (shaHex.length() < 2) {
                    hexStr.append(0);
                }
                hexStr.append(shaHex);
            }
            return Optional.of(hexStr.toString().trim());
        } catch (NoSuchAlgorithmException e) {
            log.error("没有微信加解密算法sha1");
            return Optional.empty();
        }
    }

    private static class PKCS7Encoder {
        static Charset CHARSET = StandardCharsets.UTF_8;
        static int BLOCK_SIZE = 32;

        /**
         * 获得对明文进行补位填充的字节.
         *
         * @param count 需要进行填充补位操作的明文字节个数
         * @return 补齐用的字节数组
         */
        static byte[] encode(int count) {
            int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
            if (amountToPad == 0) {
                amountToPad = BLOCK_SIZE;
            }
            char padChr = chr(amountToPad);
            StringBuilder tmp = new StringBuilder();
            for (int index = 0; index < amountToPad; index++) {
                tmp.append(padChr);
            }
            return tmp.toString().getBytes(CHARSET);
        }

        /**
         * 删除解密后明文的补位字符
         *
         * @param decrypted 解密后的明文
         * @return 删除补位字符后的明文
         */
        static byte[] decode(byte[] decrypted) {
            int pad = decrypted[decrypted.length - 1];
            if (pad < 1 || pad > 32) {
                pad = 0;
            }
            return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
        }

        /**
         * 将数字转化成ASCII码对应的字符，用于对明文进行补码
         *
         * @param a 需要转化的数字
         * @return 转化得到的字符
         */
        static char chr(int a) {
            byte target = (byte) (a & 0xFF);
            return (char) target;
        }

    }

    private static class ByteGroup {
        ArrayList<Byte> byteContainer = new ArrayList<>();

        public byte[] toBytes() {
            byte[] bytes = new byte[byteContainer.size()];
            for (int i = 0; i < byteContainer.size(); i++) {
                bytes[i] = byteContainer.get(i);
            }
            return bytes;
        }

        public void addBytes(byte[] bytes) {
            for (byte b : bytes) {
                byteContainer.add(b);
            }
        }

        public int size() {
            return byteContainer.size();
        }
    }

    private static class WeChatBizCrypt {
        byte[] aesKey;
        MiniApp miniApp;
        static Charset CHARSET = StandardCharsets.UTF_8;

        public WeChatBizCrypt(MiniApp miniApp) {
            this.miniApp = miniApp;
            if (miniApp.getEncodingAESKey().length() != 43) {
                throw new BusinessException("encodingAesKey Illegal AesKey");
            }
            this.aesKey = Base64.getDecoder().decode(miniApp.getEncodingAESKey() + "=");
        }

        // 生成4个字节的网络字节序
        byte[] getNetworkBytesOrder(int sourceNumber) {
            byte[] orderBytes = new byte[4];
            orderBytes[3] = (byte) (sourceNumber & 0xFF);
            orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
            orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
            orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
            return orderBytes;
        }

        // 还原4个字节的网络字节序
        int recoverNetworkBytesOrder(byte[] orderBytes) {
            int sourceNumber = 0;
            for (int i = 0; i < 4; i++) {
                sourceNumber <<= 8;
                sourceNumber |= orderBytes[i] & 0xff;
            }
            return sourceNumber;
        }

        /**
         * 随机生成16位字符串
         *
         * @return 16位随机字符串
         */
        String getRandomStr() {
            String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            Random random = new Random();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 16; i++) {
                int number = random.nextInt(base.length());
                sb.append(base.charAt(number));
            }
            return sb.toString();
        }

        /**
         * 对明文进行加密.
         *
         * @param text 需要加密的明文
         * @return 加密后base64编码的字符串
         */
        String encrypt(String randomStr, String text) {
            ByteGroup byteCollector = new ByteGroup();
            byte[] randomStrBytes = randomStr.getBytes(CHARSET);
            byte[] textBytes = text.getBytes(CHARSET);
            byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
            byte[] appIdBytes = miniApp.getAppId().getBytes(CHARSET);

            // randomStr + networkBytesOrder + text + appid
            byteCollector.addBytes(randomStrBytes);
            byteCollector.addBytes(networkBytesOrder);
            byteCollector.addBytes(textBytes);
            byteCollector.addBytes(appIdBytes);

            // ... + pad: 使用自定义的填充方式对明文进行补位填充
            byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
            byteCollector.addBytes(padBytes);
            byte[] unencrypted = byteCollector.toBytes();

            try {
                Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
                SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
                IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
                cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
                byte[] encrypted = cipher.doFinal(unencrypted);
                return Base64.getEncoder().encodeToString(encrypted);
            } catch (Exception e) {
                log.error("AesException EncryptAESError,{}", e.getMessage());
                throw new BusinessException("AesException EncryptAESError");
            }
        }

        /**
         * 对密文进行解密.
         *
         * @param text 需要解密的密文
         * @return 解密得到的明文
         */
        String decrypt(String text) {
            byte[] original;
            try {
                // 设置解密模式为AES的CBC模式
                Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
                SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
                IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
                cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);

                // 使用BASE64对密文进行解码
                byte[] encrypted = Base64.getDecoder().decode(text);

                // 解密
                original = cipher.doFinal(encrypted);
            } catch (Exception e) {
                e.printStackTrace();
                log.error("AesException DecryptAESError");
                throw new BusinessException("AesException DecryptAESError");
            }

            String xmlContent, fromAppId;
            try {
                // 去除补位字符
                byte[] bytes = PKCS7Encoder.decode(original);

                // 分离16位随机字符串,网络字节序和AppId
                byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

                int xmlLength = recoverNetworkBytesOrder(networkOrder);

                xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
                fromAppId = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
            } catch (Exception e) {
                log.error("AesException IllegalBuffer");
                throw new BusinessException("AesException IllegalBuffer");
            }
            if (!fromAppId.equals(miniApp.getAppId())) {
                throw new BusinessException("AesException ValidateAppidError");
            }
            return xmlContent;
        }

    }
}